脚手架 cli:generate 命令(AI 加持版)
这节来实现脚手架的 generate 命令。
不知道大家有没有用过 nest 的 cli。
先用一下:
npm install -g @nestjs/cli
nest new hello-nest进入项目,把它跑起来:
npm run start:dev浏览器访问下:
然后用下它的 generate 命令:
nest g resource aaa它就是选择生成的类型,然后就会创建这些文件,并且更新 AppModule
aaa 目录下的文件就是简单的写入文件就行,而修改 AppModule 这个是基于 AST。
因为 AppModule 的格式是规定的,所以通过 AST 能精准修改代码。
安装下依赖,再次跑:
npm install --save @nestjs/mapped-types
npm run start:dev这样用 generate 能直接生成 aaa 的 CRUD 代码。
就很方便。
这也是 generate 命令的意义。
也可以实现这样一个 generate 命令。
但只是生成组件的基本结构意义不大,完全可以结合 AI 来生成符合需求的代码。
来写一下,创建 generate 包:
mkdir packages/generate
cd packages/generate
npm init -y创建 generate 包,然后改下 package.json
"name": "@guang-cli/generate",
"publishConfig": {
"access": "public"
},在 generate 包下创建 tsconfig.json
pnpm --filter generate exec npx tsc --init改下内容:
{
"compilerOptions": {
"outDir": "dist",
"types": [ "node" ],
"target": "es2016",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"sourceMap": true
},
"include": [
"src/**/*.ts"
]
}修改 package.json 的 type 为 module
并且注册下代码入口 main 和 ts 类型 types
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",然后开始写代码。
创建 src/index.ts
import { select, input, confirm } from '@inquirer/prompts';
import os from 'node:os';
import path from 'node:path';
async function generate() {
let componentDir = '';
while(!componentDir) {
componentDir = await input({ message: '生成组件的目录', default: 'src/components' });
}
let componentDesc = '';
while(!componentDesc) {
componentDesc = await input({ message: '组件描述(尽量详细一些)', default: '生成一个 Table 组件,有包含 name、age、email 属性的 data 数组参数' });
}
console.log(componentDir, componentDesc)
}
generate();
export default generate;安装下依赖:
pnpm --filter generate add @inquirer/prompts跑一下:
pnpm --filter generate exec npx tsc -w
pnpm --filter generate exec node ./dist/index.jsnpx tsc -w 会监听文件变化实时编译,就不用每次手动 npx tsc 编译了。
然后引入 openai 来对接 AI 接口:
pnpm --filter generate add openai上节是用的 tools (也就是 function call)实现的代码提取,这次通过正则提取。
改下 src/index.ts
import { select, input, confirm } from '@inquirer/prompts';
import os from 'node:os';
import path from 'node:path';
import OpenAI from 'openai';
import fs from 'node:fs';
const client = new OpenAI({
apiKey: '你的 API KEY',
baseURL: '你的代理商的 BASE URL'
});
const systemContent = `
# Role: 前端工程师
## Profile
- author: 神光
- language: 中文
- description: 你非常擅长写 React 组件
## Goals
- 根据用户需求生成组件代码
## Skills
- 熟练掌握 typescript
- 会写高质量的 React 组件
## Constraints
- 用到的组件来源于 antd
- 样式用 scss 写
## Workflows
根据用户描述生成的组件,规范如下:
组件包含 4 类文件:
1、index.ts
这个文件中的内容如下:
export { default as [组件名] } from './[组件名]';
export type { [组件名]Props } from './interface';
2、interface.ts
这个文件中的内容如下,请把组件的props内容补充完整:
interface [组件名]Props {}
export type { [组件名]Props };
4、[组件名].tsx
这个文件中存放组件的真正业务逻辑,不能编写内联样式,如果需要样式必须在 5、styles.ts 中编写样式再导出给本文件用
5、styles.scss
这个文件中必须用 scss 给组件写样式,导出提供给 4、[组件名].tsx
每个文件之间通过这样的方式分隔:
# [目录名]/[文件名]
目录名是用户给出的组件名
## Initialization
作为前端工程师,你知道你的[Goals],掌握技能[Skills],记住[Constraints], 与用户对话,并按照[Workflows]进行回答,提供组件生成服务
`
async function generate() {
let componentDir = '';
while(!componentDir) {
componentDir = await input({ message: '生成组件的目录', default: 'src/components' });
}
let componentDesc = '';
while(!componentDesc) {
componentDesc = await input({ message: '组件描述', default: '生成一个 Table 的 React 组件,有包含 name、age、email 属性的 data 数组参数' });
}
const res = await client.chat.completions.create({
model: "gpt-4",
messages: [
{role: 'system', content: systemContent},
{role: 'user', content: componentDesc}
]
});
console.log(res.choices[0].message.content || '')
}
generate();
export default generate;在 system 的内容里指定返回内容的格式
每个文件之间通过这样的方式分隔:
# [目录名]/[文件名]
目录名是用户给出的组件名跑一下:
pnpm --filter generate exec node ./dist/index.js可以看到,返回的格式就是指定的:
这样解析起来不就简单了么?
在 astexplorer.net 里看下:
切换到 Marddownd 的 remark 编译器,可以看到 parse 出的 AST:
通过 AST 非常容易就能拿到文件名和文件内容。
安装下 remark 还有 fs-extra
pnpm --filter generate add fs-extra remark
pnpm --filter generate add --save-dev @types/fs-extra调用 remark 来解析 markdown,拿到文件路径和内容,写入即可:
这里要拼接上之前用户填入的组件目录,加上解析出的文件路径和文件内容,写入磁盘即可。
这里的 ast 结构就是从 astexplorer.net 上查看的:
import fse from 'fs-extra';
import {remark} from 'remark'const markdown = res.choices[0].message.content || '';
await remark().use(function(...args) {
return function(tree: any) {
let curPath = '';
for(let i = 0; i< tree.children.length; i++ ) {
const node = tree.children[i];
if(node.type === 'heading') {
curPath = path.join(componentDir, node.children[0].value);
} else {
try {
fse.ensureFileSync(curPath);
fse.writeFileSync(curPath, node.value);
console.log('文件创建成功:', curPath)
} catch(e) {
}
}
}
}
}).process(markdown);跑一下:
pnpm --filter generate exec node ./dist/index.js创建成功。
再加上 ora 来做下 loading:
pnpm --filter generate add ora这样,我们的 AI 版 generate 命令就跑通了。
代码上传了文档仓库
总结
这节实现了 AI 版的 generate 命令。
输入生成组件的目录、组件描述,就会调用 AI 来生成代码,然后写入组件目录。
通过在 system 里设置组件规范来保证返回的代码是符合规范的。
并且通过 system 设置规定了返回内容的格式,然后用 remark 通过 AST 解析出文件名和文件内容、写入磁盘。
这样,就可以通过 AI 生成符合规范的代码了。
当然,现在的很多内容都是写死的,比如 system 设置、API KEY、BASE URL,这些都应该是在配置文件里配置的,下节加上配置文件。